using System;
using System.Text;

namespace GeoFramework.Gps.Nmea
{
	/// <summary>
	/// Represents an NMEA sentence containing common position and precision information.
	/// </summary>
	public sealed class GpggaSentence : NmeaSentence
    {
        #region Private variables

        private Position pPosition;
		private DateTime pUtcDateTime;
		private DifferentialGps pDifferentialGps = new DifferentialGps();
		private FixQuality pFixQuality;
		private int pFixedSatelliteCount;
		private Distance pAltitude;
		private Distance pAltitudeAboveEllipsoid;
		private Distance pGeoidalSeparation;
		private DilutionOfPrecision pHorizontalDilutionOfPrecision;

        #endregion

        #region Static members

        /// <summary>
        /// Controls the number of digits of precision written in latitude and longitude measurements.
        /// </summary>
        /// <remarks>
        /// This property allows sentences to include greater precision when they are written out as strings.
        /// By default, latitude and longitude values are written to only four digits of precision, yet as
        /// many as nine digits are required to preserve sub-millimeter accuracy.  Changing this value from
        /// its default of four to a number as high as 12 will maximize precision.  This property defaults to
        /// four in order to comply with the NMEA standard.
        /// </remarks>
        public static int MaximumPrecisionDigits = 4;

        /// <summary>Returns a $GPGGA sentence with no value.</summary>
        public static readonly GpggaSentence Empty = new GpggaSentence(DateTime.Now.ToUniversalTime(),
            Position.Empty, FixQuality.NoFix, 0, DilutionOfPrecision.Maximum, Distance.Empty,
            Distance.Empty, TimeSpan.Zero, 0);

        #endregion

        #region Constructors

        public GpggaSentence(string sentence) 
            : base(sentence)
		{
			// Is the checksum valid?
			if (!IsValid && !IsPartialSentenceParsingAllowed)
			{
				pPosition = Position.Empty;
				pUtcDateTime = DateTime.MinValue;
				pFixQuality = FixQuality.NoFix;
				pAltitude = Distance.Empty;
				pAltitudeAboveEllipsoid = Distance.Empty;
				pGeoidalSeparation = Distance.Empty;
				pHorizontalDilutionOfPrecision = DilutionOfPrecision.Maximum;
				// No.  Do no parsing
				return;
			}

			// $GPGGA,hhmmss.ss,ddmm.mmmm,n,dddmm.mmmm,e,q,ss,y.y,a.a,z,g.g,z,t.t,iii*CC
			// hhmmss.ss		in UTC (coordinated universal time zone).  UTC used be known as GMT.
			// ddmm.mmmm,N	  latitude of the GPS position fix
			// dddmm.mmmm,W	 longitude of the GPS position fix
			// q				quality of the GPS fix (1 = fix, but no differential correction)
			// ss			   number of satellites being used
			// y.y			  horizontal dillution of precision
			// a.a,M			Gps antenna Distance in meters
			// g.g,M			Geoidal separation in meters
			// t.t			  age of the deferrential correction data
			// iiii			 deferential station's ID
			// *CC			  checksum for the sentence
			// Set the time
			try
			{
				if(WordCount > 1 && Words[1].Length != 0)
				{
					// Use the current satellite-derived date
					DateTime pToday = GeoFramework.IO.Devices.CurrentUtcDateTime;
					// Extract hours, minutes, and seconds
					int UTCHours = int.Parse(Words[1].Substring(0, 2), NmeaCultureInfo);
					int UTCMinutes = int.Parse(Words[1].Substring(2, 2), NmeaCultureInfo);

                    // Parse seconds and milliseconds as a float
                    float UTCSecondsRaw = float.Parse(Words[1].Substring(4), NmeaCultureInfo);

                    // Extract the integer portion of the seconds value
#if Framework20 && !PocketPC
                    int UTCSeconds = Convert.ToInt32(Math.Truncate(UTCSecondsRaw)) % 60;
#else
                    int UTCSeconds = Truncate(UTCSecondsRaw) % 60;
#endif
                    // Parse the milliseconds portion of the value
                    int UTCMilliseconds = Convert.ToInt32((UTCSecondsRaw - UTCSeconds) * 1000.0);

                    // Build the date and time, including milliseconds
#if Framework20
                    pUtcDateTime = new DateTime(pToday.Year, pToday.Month, pToday.Day, UTCHours, UTCMinutes, UTCSeconds, UTCMilliseconds, DateTimeKind.Utc);
#else
    				pUtcDateTime = new DateTime(pToday.Year, pToday.Month, pToday.Day, UTCHours, UTCMinutes, UTCSeconds, UTCMilliseconds);
#endif
				}
				else
				{
					pUtcDateTime = DateTime.MinValue;
				}

				// Update the GPS position
				if(WordCount > 5 && Words[2].Length != 0 && Words[3].Length != 0 && Words[4].Length != 0 && Words[5].Length != 0)
				{
#if PocketPC && !Framework20
                    int a = int.Parse(Words[2].Substring(0, 2), NmeaCultureInfo);
                    double b = double.Parse(Words[2].Substring(2), NmeaCultureInfo);
                    LatitudeHemisphere c = (Words[3] == "N" ? LatitudeHemisphere.North : LatitudeHemisphere.South);
                    Latitude d = new Latitude(int.Parse(Words[2].Substring(0, 2), NmeaCultureInfo),
                        double.Parse(Words[2].Substring(2), NmeaCultureInfo),
                        Words[3] == "N" ? LatitudeHemisphere.North : LatitudeHemisphere.South);
						
                    int e = int.Parse(Words[4].Substring(0, 3), NmeaCultureInfo);
                    double f = double.Parse(Words[4].Substring(3), NmeaCultureInfo);
                    LongitudeHemisphere g = (Words[5] == "E" ? LongitudeHemisphere.East : LongitudeHemisphere.West);
                    Longitude h = new Longitude(int.Parse(Words[4].Substring(0, 3), NmeaCultureInfo),
                        double.Parse(Words[4].Substring(3), NmeaCultureInfo),
                        Words[5] == "E" ? LongitudeHemisphere.East : LongitudeHemisphere.West);					

                    pPosition = new Position(d, h);
#else
					pPosition = new Position(
						new Latitude(int.Parse(Words[2].Substring(0, 2), NmeaCultureInfo),
						double.Parse(Words[2].Substring(2), NmeaCultureInfo),
						Words[3] == "N" ? LatitudeHemisphere.North : LatitudeHemisphere.South)
						, 
						new Longitude(int.Parse(Words[4].Substring(0, 3), NmeaCultureInfo),
						double.Parse(Words[4].Substring(3), NmeaCultureInfo),
						Words[5] == "E" ? LongitudeHemisphere.East : LongitudeHemisphere.West)
						);
#endif
				}
				else
				{
					pPosition = Position.Empty;
				}

				if(WordCount > 6)
				{
					switch(Words[6])
					{
                        case "0":
                            pFixQuality = FixQuality.NoFix;
                            pDifferentialGps.SetIsActive(false);
                            break;
                        case "1":
							pFixQuality = FixQuality.GpsFix;
							pDifferentialGps.SetIsActive(false);
							break;
						case "2":
							pFixQuality = FixQuality.DifferentialGpsFix;
							pDifferentialGps.SetIsActive(true);
							break;
                        case "3":
                            pFixQuality = FixQuality.Estimated;
                            pDifferentialGps.SetIsActive(false);
                            break;
                        case "4":
                        case "5":
                            pFixQuality = FixQuality.RealTimePositionAmbiguitiesFixed;
                            pDifferentialGps.SetIsActive(true);
                            break;
                        default:
                            pFixQuality = FixQuality.GpsFix;
							break;
					}
				}
				else
				{
					pFixQuality = FixQuality.NoFix;
					pDifferentialGps.SetIsActive(false);
				}

                if (WordCount > 7 && Words[7].Length > 0)
					pFixedSatelliteCount = int.Parse(Words[7], NmeaCultureInfo);
				else
					pFixedSatelliteCount = 0;

				
				if(WordCount > 8 && Words[8].Length > 0)
					pHorizontalDilutionOfPrecision = new DilutionOfPrecision(double.Parse(Words[8], NmeaCultureInfo));
				else
					pHorizontalDilutionOfPrecision = DilutionOfPrecision.Poor;
				
				// Update the Altitude
				if(WordCount > 9 && Words[9].Length != 0)
					pAltitude = new Distance(double.Parse(Words[9], NmeaCultureInfo), DistanceUnit.Meters).ToLocalUnitType();
				else
					pAltitude = Distance.Empty;

				// Parse differential GPS information
				if(WordCount > 11 && Words[11].Length != 0)
				{
					pAltitudeAboveEllipsoid = new Distance(double.Parse(Words[11], NmeaCultureInfo), DistanceUnit.Meters).ToLocalUnitType();
				}
				else
				{
					pAltitudeAboveEllipsoid = Distance.Empty;
				}

				// Set the differential GPS age
				if(WordCount > 13 && Words[13].Length != 0)
					pDifferentialGps.SetAge(pUtcDateTime, TimeSpan.FromSeconds(double.Parse(Words[13], NmeaCultureInfo)));
				else
					pDifferentialGps.SetAge(DateTime.MinValue, TimeSpan.Zero);

				// Set the differential GPS Station ID
				if(WordCount > 14 && Words[14].Length != 0)
					pDifferentialGps.SetStationId(int.Parse(Words[14], NmeaCultureInfo));
				else
					pDifferentialGps.SetStationId(0);
			}
			catch(Exception ex)
			{
				throw new NmeaSentenceException(this, "A $GPGGA sentence contained unrecognizable data.", ex);
			}
		}

		/// <summary>Creates a new instance.</summary>
		public GpggaSentence(byte[] bytes) 
            : this(ASCIIEncoding.ASCII.GetString(bytes, 0, bytes.Length))
		{
		}

		public GpggaSentence(DateTime utcDateTime, Position position, FixQuality fixQuality,
			int fixedSatelliteCount, DilutionOfPrecision horizontalDilutionOfPrecision,
			Distance altitude, Distance geoidalSeparation, TimeSpan differentialGpsAge,
			int differentialGpsStationId)
		{
			pUtcDateTime = utcDateTime;
			pPosition = RoundToNmea(position);
			pFixQuality = fixQuality;
			pFixedSatelliteCount = fixedSatelliteCount;
			pHorizontalDilutionOfPrecision = horizontalDilutionOfPrecision;
			pAltitude = altitude;
			pGeoidalSeparation = geoidalSeparation;
			pDifferentialGps.SetAge(pUtcDateTime, differentialGpsAge); 
			pDifferentialGps.SetStationId(differentialGpsStationId);
			// Build the sentence from the position information
			// Output the information as a GPGGA sentence (!!)
			StringBuilder Sentence = new StringBuilder();
			Sentence.Append("$GPGGA,");
			// Convert UTC data time to a string
			Sentence.Append(UtcDateTime.Hour.ToString(NmeaCultureInfo).PadLeft(2, '0'));
			Sentence.Append(UtcDateTime.Minute.ToString(NmeaCultureInfo).PadLeft(2, '0'));
			Sentence.Append(UtcDateTime.Second.ToString(NmeaCultureInfo).PadLeft(2, '0'));
            Sentence.Append(".");
            Sentence.Append(UtcDateTime.Millisecond.ToString(NmeaCultureInfo).PadLeft(3, '0'));
            Sentence.Append(",");            
			// Append latitude
            Sentence.Append(Position.Latitude.ToString("hhmm." + "".PadLeft(MaximumPrecisionDigits, 'm'), NmeaCultureInfo).Replace("-", ""));
            Sentence.Append(",");
			Sentence.Append(Position.Latitude.ToString("i"));
			Sentence.Append(",");
			// Append longitude
            Sentence.Append(Position.Longitude.ToString("hhhmm." + "".PadLeft(MaximumPrecisionDigits, 'm'), NmeaCultureInfo).Replace("-", ""));
            Sentence.Append(",");
			Sentence.Append(Position.Longitude.ToString("i"));
			Sentence.Append(",");
			// Append fix quality
			switch(pFixQuality)
			{
				case FixQuality.NoFix:
					Sentence.Append("0");
					break;
				case FixQuality.GpsFix:
					Sentence.Append("1");
					break;
				case FixQuality.DifferentialGpsFix:
					Sentence.Append("2");
					break;
				case FixQuality.Estimated:
					Sentence.Append("3");
					break;
				default:
					Sentence.Append("0");
					break;
			}
			Sentence.Append(",");
			// Append fixed satellite count
			Sentence.Append(FixedSatelliteCount.ToString(NmeaCultureInfo).PadLeft(2, '0'));
			Sentence.Append(",");
			// Append HDOP
			Sentence.Append(HorizontalDilutionOfPrecision.Value.ToString("#.#", NmeaCultureInfo));
			Sentence.Append(",");
			// Append altitude
			Sentence.Append(Altitude.ToMeters().ToString("v.v", NmeaCultureInfo));
			Sentence.Append(",M,");
			// Append geoidal separation
			Sentence.Append(GeoidalSeparation.ToMeters().ToString("v.v", NmeaCultureInfo));
			Sentence.Append(",M,");
			// Append differential GPS age
			double Age = Math.Round(DifferentialGps.Age.Milliseconds / 1000.0, 1);
			Sentence.Append(Age.ToString("#.#", NmeaCultureInfo).PadLeft(3, '0'));
			Sentence.Append(",");
			// Append differential GPS station ID
			Sentence.Append(DifferentialGps.StationId.ToString(NmeaCultureInfo).PadRight(4, '0'));
			// Set the sentence
			SetSentence(Sentence.ToString());
			SetChecksum();
        }

        #endregion

        #region Public members

        /// <summary>Returns the current location on Earth.</summary>
		public Position Position
		{
			get
			{
				return pPosition;
			}
		}

		/// <summary>Returns the number of satellites involved in the current fix.</summary>
		public int FixedSatelliteCount
		{
			get
			{
				return pFixedSatelliteCount;
			}
		}

		/// <summary>Returns the current satellite-derived time.</summary>
		public DateTime UtcDateTime
		{
			get
			{
				return pUtcDateTime;
			}
		}

		/// <summary>
		/// Indicates whether a satellite fix is currently in progress.
		/// </summary>
		public bool IsFixObtained
		{
			get
			{
				// Update the fix flag
				return 
					pFixQuality == FixQuality.DifferentialGpsFix 
					| pFixQuality == FixQuality.GpsFix 
					| pFixQuality == FixQuality.RealTimePositionAmbiguitiesFixed 
					| pFixQuality == FixQuality.RealTimePositionAmbiguitiesNotFixed;
			}
		}

		/// <summary>Returns the quality of the current fix.</summary>
		public FixQuality FixQuality
		{
			get
			{
				return pFixQuality;
			}
		}

		/// <summary>Returns the current precision as it relates to latitude and longitude.</summary>
		public DilutionOfPrecision HorizontalDilutionOfPrecision
		{
			get
			{
				return pHorizontalDilutionOfPrecision;
			}
		}

		/// <summary>Returns the current altitude above sea level.</summary>
		public Distance Altitude
		{
			get
			{
				return pAltitude;
			}
		}
		
		/// <summary>Returns the current altitude above the current ellipsoid.</summary>
		public Distance AltitudeAboveEllipsoid
		{
			get
			{
				return pAltitudeAboveEllipsoid;
			}
		}

		/// <summary>
		/// Returns the distance between the actual surface of the earth and the surface of
		/// the ellipsoid.
		/// </summary>
		public Distance GeoidalSeparation
		{
			get
			{
				return pGeoidalSeparation;
			}
		}

		/// <summary>Returns additional fix information during a differential (DGPS) fix.</summary>
		public DifferentialGps DifferentialGps
		{
			get
			{
				return pDifferentialGps;
			}
		}

		/// <summary>Returns a new instance by parsing the specified string.</summary>
		/// <remarks>This method is typically used by NMEA interpreters to convert a GPGGA sentence 
		/// into object form.  This method is capable of parsing any output created by the ToString
		/// method of this class.</remarks>
		public static GpggaSentence Parse(string sentence)
		{
			return new GpggaSentence(sentence);
		}

		/// <summary>
		/// Indicates that this sentence is supported by GPS.NET.
		/// </summary>
		public override bool IsSupported
		{
			get
			{
				return true;
			}
        }

        #endregion
    }
}
